﻿using System.Collections;
using System.Collections.Generic;
using System.Reflection;
using System.Text;
using JasonSoft.ScriptAnnotations;
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;
using System;
using System.Linq;
using Newtonsoft.Json.Serialization;

namespace JasonSoft.ScriptAnnotations
{
    public class AnnotatedJsonConverter : JsonConverter
    {
        private readonly Type[] _types;
        private readonly JTokenWriter _jsonWriter = new JTokenWriter();
        private JsonSerializer _jsonSerializer;
        private readonly ValidatorStrategyFactory _factory = new ValidatorStrategyFactory();

        public AnnotatedJsonConverter(params Type[] types)
        {
            _types = types;
        }

        public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer)
        {
            if (value == null)
            {
                return;
            }

            if(_jsonSerializer == null)
                _jsonSerializer = new JsonSerializer { ContractResolver = serializer.ContractResolver};

            _jsonSerializer.StringEscapeHandling = StringEscapeHandling.EscapeNonAscii;

            _jsonSerializer.Serialize(_jsonWriter, value);

            var token = _jsonWriter.Token;
            
            var valueType = value.GetType();

            if (token.Type == JTokenType.Array)
            {
                writer.WriteStartArray();
                var genericType = valueType.GetGenericArguments().Single();

                foreach (var val in (IEnumerable) value)
                {
                    _jsonSerializer.Serialize(_jsonWriter, val);
                    token = _jsonWriter.Token;
                    ExtendProperties(val, token, genericType);
                    token.WriteTo(writer);
                }

                writer.WriteEndArray();
            }
            else
            {
                ExtendProperties(value, token, valueType);
                token.WriteTo(writer);   
            }

                      
        }
            
        private void ExtendProperties(object value, JToken token, Type valueType)
        {
            if (value == null || token == null)
                return;

            foreach (var prop in token.Children().Where(c => c.Type == JTokenType.Property).Cast<JProperty>())
            {
                prop.Value = BuildJToken(prop,
                    valueType.GetProperty(prop.Name, BindingFlags.IgnoreCase | BindingFlags.Public | BindingFlags.Instance).CustomAttributes);

                if (valueType.GetProperty(prop.Name) == null)
                {
                    continue;
                }

                ExtendProperties(valueType.GetProperty(prop.Name).GetValue(value), prop.Value, value.GetType());
            }
        }

        private JToken BuildJToken(JProperty jProperty, IEnumerable<CustomAttributeData> attributes)
        {
            var rulesExtenders = new StringBuilder();

            foreach (var extender in attributes.Select(attr => _factory.GetValidatorStrategy(attr.AttributeType.Name)
                .WriteExtender(jProperty.Value, 
                attr.ConstructorArguments, attr.NamedArguments, 
                _jsonSerializer.ContractResolver.GetType() == typeof(CamelCasePropertyNamesContractResolver) ? ContractResolvers.CamelCase :  ContractResolvers.Default))
                .Where(extender => extender != string.Empty))
            {
                rulesExtenders.Append(string.Concat(extender, ","));
            }

            return rulesExtenders.Length == 0 ? jProperty.Value : new JRaw(string.Format("{{\"jValue\":\"{0}\",\"jExtend\":[{1}]}}", jProperty.Value, rulesExtenders.ToString().Remove(rulesExtenders.Length - 1, 1)));
        }

        public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer)
        {
            throw new NotImplementedException("Unnecessary because CanRead is false. The type will skip the converter.");
        }

        public override bool CanRead
        {
            get { return false; }
        }

        public override bool CanConvert(Type objectType)
        {
            return _types == null || _types.Length == 0 || _types.Contains(objectType);
        }
    }
}
